03 — 第一个 Three.js 项目

Three.js Journey · Bruno Simon · 难度:简单

03 — 第一个 Three.js 项目 课程海报

介绍

话不多说,现在是时候开始编码,创建我们的第一个 Three.js 场景了。

如果你已经熟悉如今的前端开发工具,比如 Node.js 和 Vite,你大概可以跳过本节课的大部分内容。

否则,请跟着我们一起走,别担心,我会把一切都详细讲清楚。

本地服务器和构建工具

为了运行我们即将诞生的 Three.js 网站,我们需要运行一个本地服务器。

我们可以把 HTML 文件创建在电脑上的某个地方,然后双击打开,但出于安全原因,浏览器会限制这种打开方式的网站功能。这样一来,我们无法加载 Three.js、模型、纹理等等。

解决这个问题最简单的办法,就是使用"构建工具"(build tool)或"打包器"(bundler)。

构建工具的现状

如今可用的构建工具有很多,你可能听说过其中一些,比如 Webpack、Vite、Gulp、Parcel 等等。

它们各有各的功能和优缺点,但在接下来的课程中,我们会使用一个非常特定的工具。

如今,最流行的构建工具是 Webpack。它被广泛使用,社区庞大,而且用它你能做很多事情。但虽然 Webpack 是最流行的,却不是最受好评的。

事实上,这些天最受好评的构建工具是 Vite(法语中"快速"的意思,发音为 /vit/,类似"veet")。它安装更快、运行更快,也更不容易出 bug。归根结底,开发者体验要好得多。

最初,所有 Three.js Journey 的练习都在 Webpack 上运行,下面有些课程也是那样录制的。但现在,练习都跑在 Vite 上了。别担心,我把 Vite 配置成让文件和 Vite 的行为看起来和 Webpack 配置非常相似,你可能在看视频时甚至察觉不到差异。

Vite

这堂课不是讲 Vite 的,但因为我忍不住,我还是会解释一下它的几个方面。

正如前面提到的,Vite 是一个构建工具。思路是我们要编写 HTML/CSS/JS 这样的 Web 代码,然后由 Vite 构建出最终的网站。它还会做一堆事情,比如优化、缓存破坏(cache breaking)、源码映射、运行本地服务器等等。

虽然 Vite 能满足最基本的需求,但我们还可以添加插件来处理更多功能,比如各种语言或特殊文件。实际上,我们会在课程后面添加插件,以便能处理 GLSL 文件来创建自定义 shader,同时也能运行 React。

Vite 由 Vue.js 的创造者 Evan You 创建,它有数百位开发者持续维护,而且人气正高。

第一个 Vite 项目

你可能已经注意到这节课附带了一个 starter(起始)文件。在后面的课程里,你还会看到 final(最终)文件。

starter 文件是你跟随练习开始时需要用到的,而 final 文件是练习的最终结果,以备你需要参考。但这次我们不会马上用 starter 文件,我会先演示如何从零开始一个项目。到后面阶段,我们会用到那个文件。

终端

我们会在终端中运行一些命令。我们可以使用 MacOS 的 Terminal(终端)或 Windows 的 Command Prompt(命令提示符),但我实际上推荐你使用 VSCode 内置的终端(如果你像我一样用 VSCode 的话)。启动 VSCode,按 CMD + J(MacOS)或 CTRL + J(Windows)打开内置终端。

如果这样做,无论你用什么操作系统,下面的命令都是一样的。

终端的工作方式有点像文件资源管理器,你需要在正确的文件夹里才能运行命令。

你可以用 cd 后面跟文件夹名来进入其中。你也可以输入 cd (末尾带一个空格),然后直接把文件夹拖放进去。你可以用 pwd 测试当前所在位置,用 ls 列出当前文件夹中的文件。

故障排除

我们会执行一堆命令和操作。如果你遇到任何问题,在课程末尾附近有一节我列出了经典问题的章节。如果仍然无法解决,作为 Three.js Journey 的成员,你可以访问一个私密的 Discord 服务器。使用顶部菜单进入 Discord 页面,在那里分享你的问题。我们会帮你解决。

Node.js

要运行 Vite,你需要在电脑上安装 Node.js。

Node.js 让你能在浏览器之外、在电脑上运行 JavaScript。它非常适合运行像 Vite 这样各种工具,它已经存在很多年了,而且非常流行。

如果你不确定 Node.js 是否已经安装,或者安装的是什么版本,在终端中运行 node -v 即可。

如果输出提示 node 命令无法识别,那就说明没有安装。

如果已安装,输出会包含当前版本号。请确保它更新到最新版本。Vite 目前可运行在 14.18 及以上版本,但我建议你始终安装 Node.js 的 LTS(长期支持)版本。

要安装或更新 Node.js,请访问 https://nodejs.org/en/,下载"LTS"并按默认设置安装。

关闭你的 Terminal(MacOS)或 Command Prompt(Windows),重新打开,再次运行 node -v 检查版本。

创建一个 Node.js 项目

现在我们已经有了 Node.js,也知道了如何使用终端,是时候创建我们的 Node.js 项目了。

创建一个用来存放你网站的文件夹(尽量保持条理,后面还有一大堆课程要学)。

你现在可以做的,是在 VSCode 中打开这个文件夹(如果你用 VSCode 的话)。这有两个主要原因会派上用场:

现在打开应该已经定位在该文件夹的终端,运行 npm init -y。

当我们安装 Node.js 时,会自动安装 NPM。NPM 是 Node Package Manager 的缩写,是一个依赖管理器,会帮我们拉取所需的包,比如 Three.js、Vite,以及课程中要用到的各种库。此外,NPM 附带一个名为 npm 的命令行工具,我们可以从终端使用它。

运行 npm init -y 后,NPM 会创建一个 package.json,里面包含运行一个 Node.js 项目所需的最基本信息。

添加依赖

在这节课中,我们会用到两个依赖:Vite 和 Three.js。

如果你好奇,可以在 NPM 网站上找到它们:

目前,我们只关注 Vite,稍后再添加 Three.js。要向 Node.js 项目添加依赖,在终端中运行 npm install vite。

你应该会注意到三件事。

这 2 个文件和那个文件夹实现了项目共享。如果你想把自己漂亮的 Three.js 网站分享给另一位开发者,你会想移除 node_modules 文件夹,然后分享项目其余部分。对方拿到你的项目后,运行一条命令,就会根据 package.json 中写的内容自动重建 node_modules 文件夹。

不知道怎么做?别担心,我们一会儿就会复习那条命令。注意 package-lock.json 是可选的,但如果项目里有它,对方得到的依赖就会和你完全一样,没有任何容错偏差。

基础网站

我们有了 Node.js 项目,有了作为依赖添加进来的 Vite。现在,我们需要创建一个基础网站。

仍在项目文件夹中,创建一个 index.html 文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>03 - First Three.js Project</title>
</head>
<body>
    <h1>Soon to be a Three.js website</h1>
</body>
</html>

不要直接打开这个文件!那样就违背了我们正在用 Vite 做的事。

在 package.json 中,把 "scripts" 部分替换成下面这样:

{
  // ...
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  // ...
}

有了这两个"scripts",我们现在可以从终端运行 dev 和 build 命令了,这两条命令分别通过 node_modules/ 文件夹中的 vite/ 依赖触发 vite 和 vite build。

要运行 dev 脚本,在终端中输入 npm run dev。

Vite 应该会显示一个类似 http://localhost:5173/ 的 URL。把它复制到浏览器中打开,就像打开任何网站一样。

恭喜,你的 Vite 网站已经跑起来了。

你的终端看起来可能像卡住了,但其实它现在正在运行服务器,停止这个脚本就会导致服务器关闭,我们一会儿就会这么做。

build 命令会输出网站的最终版本。不过这要等到后面的课程,我们会学习如何让网站上线。

添加 JavaScript

这很棒,但现在我们想在漂亮的网站上执行一些 JavaScript。

仍在项目文件夹中,创建一个 script.js 文件:

console.log('JavaScript is working')

在 index.html 中,把下面的 <script> 加到 <body> 的末尾:

<!-- ... -->
<body>
    <h1>Soon to be a Three.js website</h1>
    <script type="module" src="./script.js"></script>
</body>
</html>

别忘了 type="module"。

打开你的 Developer Tools(开发者工具,或简称 DevTools),你应该能在 Console(控制台)里看到 JavaScript is working。

你可能已经注意到,你甚至都不需要刷新页面。Vite 检测到了改动,会自动为你刷新页面。

Three.js

就像我们当初把 Vite 加进项目一样,现在需要把 Three.js 加进去。

添加依赖

这个依赖在 NPM 上的包名是 three。

我们需要运行 npm install three,但当前终端正忙于运行 Vite 服务器。

你有两个选择:

我们选择关闭服务器。

按 CTRL + C 停止服务器。在 Windows 系统上你可能需要按多次,或者用字母 o 确认。

现在运行 npm install three:

three/ 文件夹应该已经被添加到了 node_modules/ 文件夹中,package.json 和 package-lock.json 现在也都包含了对它的引用。

用 npm run dev 重新启动服务器:

导入 Three.js

回到 script.js,我们可以通过这样写来从 three 依赖中导入 Three.js:

import * as THREE from 'three'

这会把 Three.js 的所有核心类从 three 依赖导入到一个名为 THREE 的变量中。

这种导入依赖的方式叫做"ES modules"。我们不会深入这个主题,因为它更多属于 JavaScript 基础,而不是 Three.js。将来,在课程中需要的时候,我们还会介绍更多例子。

使用 Three.js

在我们的 script.js 文件里,现在我们有了一个名为 THREE 的变量可用。注意要始终用大写字母写它。

如果你对这个变量执行 console.log(),你会看到里面内容相当丰富:

import * as THREE from 'three'

console.log(THREE)

THREE 变量包含了一个经典 Three.js 项目中你可能需要的大部分类和属性。遗憾的是,并非所有类都在这个变量里,不过我们以后会知道如何访问它们。

要使用其中一个类,你需要实例化它。例如,如果你想创建一个场景,你会写 const scene = new THREE.Scene()。如果你想创建一个球体几何体,你需要写 const sphereGeometry = new THREE.SphereGeometry(1.5, 32, 32)——这些我们以后会深入讲解。

第一个场景

是时候创建我们的场景,在屏幕上产生点什么了。

要起步,我们需要 4 个要素:

场景

场景就像一个容器。你把你的物体、模型、粒子、灯光等等放进去,然后在某个时刻,你让 Three.js 渲染这个场景。

要创建场景,使用 Scene 类:

// Scene
const scene = new THREE.Scene()

物体

物体可以有很多种。你可以有基础几何体、导入的模型、粒子、灯光等等。

我们先从一个简单的红色立方体开始。

要创建那个红色立方体,我们需要创建一种名为 Mesh(网格)的物体。一个 Mesh 是几何体(形状)和材质(外观)的组合。

几何体和材质都有很多种,但我们现在先保持简单,创建一个 BoxGeometry 和一个 MeshBasicMaterial。

要创建几何体,我们使用 BoxGeometry 类,前 3 个参数对应盒子的尺寸。

// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)

要创建材质,我们使用 MeshBasicMaterial 类,带一个参数:一个包含所有选项的对象 {}。我们只需要指定它的 color(颜色)属性。

在 Three.js 中指定颜色有很多方式。你可以传一个 JS 十六进制数 0xff0000,你可以传一个字符串形式的十六进制 '#ff0000',你可以用像 'red' 这样的颜色名称,或者你可以在 Color 类下传一个实例——关于这个我们后面会多讲一些。

// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })

要创建最终的 mesh,我们使用 Mesh 类,并把 geometry 和 material 作为参数传进去。

// Object
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 })
const mesh = new THREE.Mesh(geometry, material)

现在你可以用 add(...) 方法把 mesh 添加到场景中:

scene.add(mesh)

如果你不把物体添加到场景里,你就看不到它。

相机

相机是不可见的。它更像是一个理论上的观察视角。当我们渲染场景时,会从这个相机的视角进行渲染。

你可以像电影片场那样拥有多台相机,并且可以随心所欲地在这些相机之间切换。通常我们只用一台相机。

相机有不同类型,我们会在以后的课程中讨论。目前,我们只需要一个能处理透视的相机(让近处的物体看起来比远处的物体更突出)。

要创建相机,我们使用 PerspectiveCamera 类。

我们需要提供两个至关重要的参数。

视场角(field of view)

视场角就是你的视野角度有多大。如果你用非常大的角度,你就能同时看到所有方向,但会有很大的畸变,因为结果要画在一个小矩形上。如果你用小角度,东西看起来就像被放大了。 视场角(或 fov)用度表示,对应垂直方向的视野角度。此次练习中我们采用 75 度的角度。

这里有一段视频讲解视场角变化看起来是什么样:

lesson demo video preview

纵横比(aspect ratio)

大多数情况下,纵横比就是 canvas 的宽度除以高度。 我们目前还没有指定任何宽度或高度,但以后会有需要。在此期间,我们会创建一个带临时值的对象,之后可以复用它。

别忘了把相机添加到场景中。即使不把相机添加到场景,一切也应该能工作,但以后可能会导致一些 bug:

// Sizes
const sizes = {
    width: 800,
    height: 600
}

// Camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
scene.add(camera)

渲染器

渲染器的工作就是做渲染。想不到吧!

我们只需请渲染器从相机的视角渲染我们的场景,结果会被绘制到一个 canvas 里。你可以自己创建 canvas,也可以让渲染器生成它,然后添加到你的页面。本次练习中,我们会把 canvas 加到 HTML 里,再传给它给渲染器。

在 index.html 中,把 <h1> 替换为 <canvas> 元素,放在加载脚本的前面,并给它一个 class:

<canvas class="webgl"></canvas>

要创建渲染器,我们使用 WebGLRenderer 类,带一个参数:一个包含所有选项的对象 {}。我们需要指定 canvas 属性,对应我们添加到页面的那个 <canvas>。

在代码开头创建一个 canvas 变量,然后用 document.querySelector(...) 抓取我们在 HTML 中创建的元素并存储进去。

最好把 canvas 赋给一个变量,因为在接下来的课程中我们还会用它做其他事情。

我们还需要用之前创建的 sizes 对象,通过 setSize(...) 方法来更新渲染器的大小。

setSize(...) 方法会自动把我们 <canvas> 相应地调整大小:

// Canvas
const canvas = document.querySelector('canvas.webgl')

// ...

// Renderer
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)

现在,你还看不到任何东西,但你的 canvas 就在那里,并且已经被相应调整大小了。如果你好奇,可以用开发者工具检查一下这个 <canvas>。

第一次渲染

是时候进行第一次渲染了。在渲染器上调用 render(...) 方法,并把 scene 和 camera 作为参数传进去:

renderer.render(scene, camera)

黑屏?我们的物体去哪了?

问题出在这:我们没有指定物体的位置,也没有指定相机的位置。两者都在默认位置,也就是场景中心,而我们不能从物体内部看到它(默认情况下)。

我们需要移动一些东西。

要做到这一点,每个物体上我们都可以访问多个属性,比如 position(位置)、rotation(旋转)和 scale(缩放)。现在,用 position 属性把相机向后移。

position 属性是一个对象,有三个相关属性:x、y 和 z。默认情况下,Three.js 把前后方向的轴视为 z。

要把相机向后移,我们需要给该属性提供一个正值。只要在创建 camera 变量之后、渲染之前,你可以在任何地方做这件事:

const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height)
camera.position.z = 3
scene.add(camera)

恭喜,你应该能看到第一次渲染了。它看起来像一个正方形,那是因为相机恰好正对着立方体,你只能看到它的一面。

不用担心渲染的大小,我们以后会学习如何让 canvas 适配视口。

在接下来的课程中,你会学到更多关于 position、rotation 和 scale 属性的知识,如何修改它们,以及如何让场景动起来。

使用 starter 文件

虽然我们已经学会了如何从零开始,但每个练习都会附带一个 starter(起始)文件,这样你就不必每次都重复整个流程了。

而且,本课也确实可以找到 starter 文件。

后面大多数课程的工作方式都一样,我会解释如何使用这些文件,以便你边看课边完成练习。

Zip 文件

下载并解压 starter 文件(在播放器正下方)。

解压文件取决于你的操作系统以及安装了哪些应用。在 MacOS 上,你可以直接双击。在 Windows 上,你应该右键点击,选择类似 Extract…(解压)的选项,然后选择解压到的文件夹等选项。

依赖

你可能已经注意到没有 node_modules/ 文件夹。正如前面提到的,分享 Node.js 项目时,不要把 node_modules/ 一起分享,因为这个文件夹可能会变得非常大。

但我们确实需要这些依赖,而它们其实都列在 package.json 文件里。要安装这些列出的依赖,在项目文件夹中打开终端并运行 npm install:

稍等片刻,等依赖被拉取下来,你应该会看到 node_modules 文件夹被创建出来,旁边还有 package-lock.json。

运行服务器

现在,要运行服务器,在项目文件夹的终端中运行 npm run dev。

等一两秒钟,网站应该会在你的默认浏览器中打开。

如果页面没有自动打开,终端应该会显示两个 URL,看起来类似 http://localhost:5173/ 和 http://192.168.1.25:5173/。

在浏览器中试试它们:

这里的配置比我们之前从零搭建的稍微高级一些。

有一个 vite.config.js,里面我提供了一些特定设置。我们不会深入细节,因为我们已经花太多时间聊 Vite 了。不过,如果你好奇,可以在 Vite 文档找到所有你需要的信息。

故障排除

如果一切都正常,你可以跳过本节。

如果你一路上遇到了问题,接下来我会列举一些常见的故障以及如何修复。

终端所在文件夹

确保你的终端是在项目文件夹中打开的。

用 pwd 命令显示终端当前所在的文件夹。

用 cd (末尾带空格)命令,把文件夹拖进去,然后按 Enter 让终端进入那个文件夹。

路径过长

如果你的项目文件夹嵌套在别的文件夹里非常深,你可能会遇到路径太长,导致 Node.js 无法处理的情况。

把文件夹往上层移动,并记得让终端也跟着移动,然后再试一次。

带版本同步的文件夹

小心 OneDrive、Google Drive、Dropbox 这类工具,它们会把你的文件"保存"到云端。

它们可能会弄乱你的 Node.js 依赖。

尽量把项目移到这些"已保存"文件夹之外。

路径中的特殊字符

避免从电脑根目录直到项目之间出现特殊字符。

理想情况下,应该只使用小写字母、数字、短横线和下划线。

MacOS 和 Xcode

MacOS 可能会要求你安装"Xcode Command Line Tools"(Xcode 命令行工具)。

这通常是无害的提示,但有时它会阻止 Node.js 项目运行。

如果你想了解更多信息,可以看看这篇文章。简单来说,只需在终端中运行 xcode-select --install,然后按照提示操作即可。

权限

有时权限会出问题,导致 NPM 无法安装依赖。

如果你清楚自己在做什么,可以尝试修复权限。否则,删除文件夹,重新下载 starter,再按步骤操作一次。

漏洞警告

在安装依赖时,NPM 可能会警告你有"vulnerabilities"(漏洞)。这些大多数时候(如果不是总是的话)是无害的。我会定期更新课程依赖以避免这些警告,但你不必太担心。

不要试图用 NPM 建议的命令去修复它们,否则你最终得到的版本可能和我们课上用的不一样,从而引发 bug。

还是不行

如果你找不到问题所在,去课程的 Discord 服务器,把你掌握的任何信息都分享出来。我们会帮你解决。

关于 Vite 配置的更多内容

关于 Vite 配置,有几点你需要了解,以及一些提醒:

结语

就是这样。我们已经准备好学习后面的许多课程,并使用各种练习了。

如果你对自己独立重做这些还不够自信,别担心,我们会在下一节课一起再做一遍。

记住,下面有些课程是用 Webpack 录制的,你可能会看到一些细微差别。那些差别你可以忽略。

我们现在做点 Three.js 怎么样?